package ChatSystem;

import java.io.IOException;
import java.util.*;
import java.net.*;

import chatsystemTDa2.*;

/**
 * The ChatSystem Network Interface. It provides high level message transfer routines meant to be used by the ChatController.
 */
public class ChatNI implements RemoteChatSystem {	
	private ChatController _controller;
	private InetAddress _group;
	private UDPReceiver _udpReceiver;
	private UDPSender _udpSender;
	private MessageSender _messageSender;
	private FileSender _fileSender;
	private boolean _connected;
		
	// ==================================================
	/**
	 * Initializes the network interface
	 * @param controller The ChatController singleton instance
	 */
	public ChatNI(ChatController controller) {
		_controller = controller;

		_udpSender = new UDPSender();
		try {
			_group = this.getBroadcastAddress();
		} catch (Exception e) {}
		
		_messageSender = new MessageSender(_udpSender);
		_fileSender = new FileSender();
	}
	
	// ==================================================
	
	/**
	 * End all network activities. This should be the final call to the NI instance.
	 */
	public void stop() {
		_messageSender.stopSender();
		_fileSender.stopSender();
	}
	
	/**
	 * Gets the TCP/UDP port used to communicate with the remote ChatSystem.
	 * @return The TCP/UDP port of the remote ChatSystem
	 */
	public int getPort() {
		return 16050;
	}
	
	// ==================================================
	
	/**
	 * Manages the reception of a Hello message from the remote ChatSystem.
	 * @param msg The Hello message
	 * @param a The address of the remote ChatSystem
	 */
	private void onHelloReceived(Hello msg, InetAddress a) {
		if(!this.isSelf(a)) {
			User user = _controller.addUser(msg.getNickname(), a);
			this.sendHelloAck(user);
		}
		else {
			_controller.getLocalUser().setAddress(a);
		}
	}
	
	/**
	 * Manages the reception of a HelloAck message from the remote ChatSystem.
	 * @param msg The HelloAck message
	 * @param a The address of the remote ChatSystem
	 */
	private void onHelloAckReceived(HelloAck msg, InetAddress a) {
		_controller.addUser(msg.getNickname(), a);
	}

	/**
	 * Acknowledge the Hello message of the remote user
	 * @param user The remote user to notify
	 */
	public void sendHelloAck(User user) {
		_udpSender.sendMessage(new HelloAck(_controller.getLocalUser().getNickname()), user.getAddress(), user.getPort());
	}

	/**
	 * Broadcasts our connection attempt and starts the network messages handling thread.
	 * @throws IOException When the network messages handling thread could not establish a connection.
	 */
	public void sendHello() throws IOException {
		_udpReceiver = new UDPReceiver(this);
		_udpReceiver.start();
		while(!_udpReceiver.running());
		
		_udpSender.sendMessage(new Hello(_controller.getLocalUser().getNickname()), _group, this.getPort());
		_connected = true;
	}
		
	/**
	 * Manages the reception of a Goodbye message from the remote ChatSystem.
	 * @param msg The Goodbye message
	 */
	private void onGoodbyeReceived(Goodbye msg) {
		_controller.removeUser(msg.getNickname());
	}

	/**
	 * Broadcasts our disconnection of the ChatSystem and stops the network messages handling thread.
	 */
	public void sendGoodbye() {
		_udpSender.sendMessage(new Goodbye(_controller.getLocalUser().getNickname()), _group, this.getPort());
		_connected = false;
		_udpReceiver.stopReceiver();
		try {
			_udpReceiver.join();
		} catch (InterruptedException e) {}
	}
	
	/**
	 * Manages the reception of a FileRequest message from the remote ChatSystem.
	 * @param request The FileRequest message
	 * @param a The address of the remote ChatSystem
	 */
	private void onFileRequestReceived(FileRequest request, InetAddress a) {
		_controller.onFileRequestReceived(_controller.getUser(a), request.getName());
	}
	
	/**
	 * Starts the TCP transfer thread which will notify the remote user of the file approval
	 * @param message The message encapsulating the file to be downloaded
	 */
	public void acceptFileRequest(ChatMessage.ChatFileMessage message) {
		new TCPReceiver(_udpSender, message).start();
	}
	
	/**
	 * Notifies the remote user that we just denied his file transfer request.
	 * @param message The message encapsulating the file to be discarded.
	 */
	public void declineFileRequest(ChatMessage.ChatFileMessage message) {
		_udpSender.sendMessage(new FileResponse(false, message.getFilename()), message.getSender().getAddress(), message.getSender().getPort());
	}

	/**
	 * Checks whether the Network Interface is connected and ready to process messages from the network.
	 * @return The state of the network interface
	 */
	public boolean isConnected() {
		return _connected;
	}
	
	/**
	 * Manages the reception of a Send message from the remote ChatSystem.
	 * @param msg The Send message
	 * @param a The address of the remote ChatSystem
	 */
	private void onMessageReceived(Send msg, InetAddress a) {
		User user = _controller.getUser(a);
		_controller.onMessageReceived(user, msg.getMessage());
		_udpSender.sendMessage(new SendAck(msg.getID()), a, this.getPort());
	}
	
	/**
	 * Forwards the message to send to the message sender.
	 * @param message The message to send
	 */
	public void sendMessage(ChatMessage.ChatTextMessage message) {
		_messageSender.postMessage(message);
	}
	
	/**
	 * Notifies the remote ChatSystem that we wish to share a file with him and forward the file to the file sender
	 * @param message The message encapsulating the file
	 */
	public void sendFileRequest(ChatMessage.ChatFileMessage message) {
		_udpSender.sendMessage(new FileRequest(message.getFilename()), message.getRecipient().getAddress(), this.getPort());
		_fileSender.postMessage(message);
	}
	
	/**
	 * Manages the reception of a FileResponse message from the remote ChatSystem.
	 * @param response The FileResponse message
	 * @param a The address of the remote ChatSystem
	 */
	private void onFileResponseReceived(FileResponse response, InetAddress a) {
		User fileRecipient = _controller.getUser(a);
		try {
			if(response.getResponse() == true) {
				_fileSender.acceptMessage(fileRecipient, response.getName());
			}
			else {
				_fileSender.declineMessage(fileRecipient, response.getName());
			}
		}
		catch(IllegalArgumentException e) {
			// Silently fail, not really good but what else? There is a bug on the remote ChatSystem…
			System.out.println("No pending file transfer matching the pair (" + fileRecipient + ", " + response.getName() + ")!");
		}
	}
	
	/**
	 * Manages the handling of the different kind of messages received on the network.
	 * @param message The generic message to be discriminated
	 * @param address The sender Internet address
	 */
	public void onMessageReceived(Message message, InetAddress address) {
		if(message instanceof Hello) {
			this.onHelloReceived((Hello)message, address);
		}
		else if(message instanceof Goodbye) {
			this.onGoodbyeReceived((Goodbye)message);
		}
		else if(message instanceof HelloAck) {
			this.onHelloAckReceived((HelloAck)message, address);
		}
		else if(message instanceof Send) {
			this.onMessageReceived((Send)message, address);
		}
		else if(message instanceof FileRequest) {
			this.onFileRequestReceived((FileRequest)message, address);
		}
		else if(message instanceof FileResponse) {
			this.onFileResponseReceived((FileResponse)message, address);
		}
	}
	
	/**
	 * Checks whether the specified Internet address refers to the local ChatSystem
	 * @param address The Internet address to test
	 * @return true if the Internet address is referring to the local ChatSystem, false otherwise
	 */
	private boolean isSelf(InetAddress address) {
		try {
			Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
			while(e.hasMoreElements()) {
				NetworkInterface n = e.nextElement();
				Enumeration<InetAddress> ee = n.getInetAddresses();
				while(ee.hasMoreElements()) {
					if(address.equals(ee.nextElement())) {
						return true;
					}
				}
			}
		}
		catch(Exception e) {}
		
		return false;
	}
	
	/**
	 * Retrieves the broadcast address used to send Hello and Goodbye messages.
	 * @return The broadcast address of the network
	 */
	private InetAddress getBroadcastAddress() {
		String OS = System.getProperty("os.name");

		if (OS.equals("Windows 7")) {
			final byte[] BROADCAST = {-1, -1, -1, -1};
			
			try {
				return InetAddress.getByAddress(BROADCAST);
			} catch (Exception e) {}
		}
		else {
			try {
				InetAddress localHost = InetAddress.getLocalHost();
				NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);

				for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
					if(address.getBroadcast() != null) {
						return address.getBroadcast();
					}
				}
			}
			catch (Exception e) {}
		}

		return null;
	}
}
